package org.jeecg.feishu.api.dto;

import org.jeecg.feishu.api.enums.AccessTokenType;
import org.jeecg.feishu.api.function.FeishuRequestOptFn;
import lombok.Data;
import lombok.ToString;
import org.springframework.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 飞书HTTP请求
 *
 * @param <I> 请求对象
 * @param <O> 请求结果返回返回对象
 */
@Data
@ToString
public class FeishuRequest<I, O> {
    /**
     * GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
     */
    private String httpMethod;
    private String httpPath;
    private String queryParams;
    private String domain;
    private String urlPrefix;
    private AccessTokenType accessTokenType;
    private boolean isOpenApi = true;
    private long timeoutOfMs;
    private boolean retry = false;
    private boolean isNotDataField;
    private I input;
    private final List<FeishuRequestOptFn> requestOptFns = new ArrayList<>();
    private O output;
    private FeishuResponse<O> response;

    public FeishuRequest(String httpPath, String httpMethod, I input, O output, FeishuRequestOptFn... requestOptFns) {
        this.httpPath = httpPath;
        this.httpMethod = httpMethod;
        this.input = input;
        this.output = output;
        this.requestOptFns.addAll(Arrays.asList(requestOptFns));
    }

    public FeishuRequest(String httpPath, String httpMethod, I input, O output, AccessTokenType accessTokenType, FeishuRequestOptFn... requestOptFns) {
        this.httpPath = httpPath;
        this.httpMethod = httpMethod;
        this.input = input;
        this.output = output;
        this.accessTokenType = accessTokenType;
        this.requestOptFns.addAll(Arrays.asList(requestOptFns));
    }

    public FeishuRequest copy() {
        FeishuRequest<I, O> requestCopy = new FeishuRequest<>(httpPath, httpMethod, input, output, accessTokenType);
        requestCopy.getRequestOptFns().addAll(requestOptFns);
        return requestCopy;
    }

    public static <I, O> FeishuRequest<I, O> newRequestByApp(String httpPath, String httpMethod, I input, O output, FeishuRequestOptFn... requestOptFns) {
        FeishuRequestOptFn[] optFns = null;
        if (requestOptFns != null) {
            optFns = new FeishuRequestOptFn[requestOptFns.length + 1];
            System.arraycopy(requestOptFns, 0, optFns, 0, requestOptFns.length);
            optFns[requestOptFns.length] = (opt) -> opt.setNotDataField(false);
        } else {
            optFns = new FeishuRequestOptFn[1];
            optFns[0] = (opt) -> opt.setNotDataField(false);
        }
        return new FeishuRequest<I, O>(httpPath, httpMethod, input, output, AccessTokenType.App, optFns);
    }

    public static <I, O> FeishuRequest<I, O> newRequestByTenant(String httpPath, String httpMethod, I input, O output, FeishuRequestOptFn... requestOptFns) {
        FeishuRequestOptFn[] optFns = null;
        if (requestOptFns != null) {
            optFns = new FeishuRequestOptFn[requestOptFns.length + 1];
            System.arraycopy(requestOptFns, 0, optFns, 0, requestOptFns.length);
            optFns[requestOptFns.length] = (opt) -> opt.setNotDataField(false);
        } else {
            optFns = new FeishuRequestOptFn[1];
            optFns[0] = (opt) -> opt.setNotDataField(false);
        }
        return new FeishuRequest<I, O>(httpPath, httpMethod, input, output, AccessTokenType.Tenant, optFns);
    }

    public static <I, O> FeishuRequest<I, O> newRequestByAuth(String httpPath, String httpMethod, I input, O output) {
        return new FeishuRequest<I, O>(httpPath, httpMethod, input, output, AccessTokenType.None, (opt) -> opt.setNotDataField(true));
    }

    public void addOptFn(FeishuRequestOptFn optFn) {
        this.requestOptFns.add(optFn);
    }

    public void init() {
        FeishuRequestOpt opt = new FeishuRequestOpt();
        for (FeishuRequestOptFn f : this.getRequestOptFns()) {
            f.fn(opt);
        }
        this.timeoutOfMs = opt.getTimeoutOfMs();
        this.isNotDataField = opt.isNotDataField();
        if (opt.getPathParams() != null) {
            this.httpPath = resolvePath(this.getHttpPath(), opt.getPathParams());
        }
        if (opt.getQueryParams() != null) {
            this.queryParams = buildQuery(opt.getQueryParams());
        }
    }

    public String resolvePath(String path, Map<String, Object> pathVar) {
        String tmpPath = path;
        StringBuilder newPath = new StringBuilder();
        for (; true; ) {
            int i = tmpPath.indexOf(":");
            if (i == -1) {
                newPath.append(tmpPath);
                break;
            }
            newPath.append(tmpPath, 0, i);
            String subPath = tmpPath.substring(i);
            int j = subPath.indexOf("/");
            if (j == -1) {
                j = subPath.length();
            }
            String varName = subPath.substring(1, j);
            Object v = pathVar.get(varName);
            if (v == null) {
                throw new IllegalArgumentException("path:" + path + ", param name:" + varName + " not found value");
            }
            newPath.append(v.toString());
            if (j == subPath.length()) {
                break;
            }
            tmpPath = subPath.substring(j);
        }
        return newPath.toString();
    }

    public String buildQuery(Map<String, Object> params) {
        if (params == null || params.isEmpty()) {
            return "";
        }
        Set<Map.Entry<String, Object>> entries = params.entrySet();
        List<String> list = new ArrayList<>();
        for (Map.Entry<String, Object> entry : entries) {
            String name = entry.getKey();
            Object value = entry.getValue();
            if (value == null) {
                continue;
            }
            if (value instanceof List) {
                for (Object o : (List) value) {
                    list.add(name + "=" + encode(o.toString()));
                }
            } else if (value.getClass().isArray()) {
                int len = Array.getLength(value);
                for (int i = 0; i < len; i++) {
                    list.add(name + "=" + encode(Array.get(value, i).toString()));
                }
            } else {
                list.add(name + "=" + encode(value.toString()));
            }
        }
        if (list.isEmpty()) {
            return "";
        }
        StringBuilder query = new StringBuilder();
        for (String s : list) {
            query.append(s).append("&");
        }
        return query.deleteCharAt(query.length() - 1).toString();
    }

    private static String encode(String value) {
        try {
            return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            // won't happen
        }
        return "";
    }

    public String url(String urlPrefix) {
        String path = null;
        if (!StringUtils.isEmpty(urlPrefix)) {
            path = "/" + urlPrefix + "/" + this.getHttpPath();
        } else {
            path = "/" + this.getHttpPath();
        }

        if (!StringUtils.isEmpty(this.getQueryParams())) {
            path += "?" + this.getQueryParams();
        }
        return path;
    }

    public String fullUrl(String domain, String urlPrefix) {
        return domain + url(urlPrefix);
    }
}
